<?php
/**
 * Optimizer
 *
 * CSS, JS 파일들의 최적화를 제공합니다.
 *
 * @package boot
 * @author  Platform team
 */

include FW_DIR_FRAMEWORK_VENDOR . '/packer/javascript.php';
include FW_DIR_FRAMEWORK_VENDOR . '/packer/css.php';

class Optimizer
{
    const TYPE_JS   = 'js';
    const TYPE_CSS  = 'css';

    /**
     * 로드 종류에 관한 키
     */
    const LOAD_RESERVED_PRE = '__pre_compile__';
    const LOAD_BEFORE       = 'before';
    const LOAD_MAIN         = 'main';
    const LOAD_AFTER        = 'after';

    /**
     * External 리소스의 URL과 CHARSET 구분자
     */
    const SP = '+|+';

    /**
     * 버젼
     * !!NOTICE!!
     * 이것의 진정한 용도는 캐시 구성방식이 바꼈을 경우 캐시를 갱신하기 위함.
     * 기존에는 해당 파일의 filemtime 값을 체크하였으나, 함수호출의 오버헤드를 줄이기 위해 수정함.
     */
    private static $sVersion = '3';

    /**
     * 스크립트 태그상에 호출 될 URL 경로
     */
    private static $sUrl = '';

    /**
     * 캐시가 생성될 루트 디렉토리
     */
    private static $sCacheroot = '';
    private static $sCachejsPath = '';
    private static $sCachecssPath = '';

    /**
     * JS 파일 목록
     */
    private static $aJs = array();

    /**
     * CSS 파일 목록
     */
    private static $aCss = array();

    /**
     * JS 소스 목록
     */
    private static $aJsSource = array();

    /**
     * CSS 소스 목록
     */
    private static $aCssSource = array();

    /**
     * JS URL 목록
     */
    private static $aJsExternal = array();

    /**
     * CSS URL 목록
     */
    private static $aCssExternal = array();

    /**
     * 구동 여부
     */
    private static $isProgress = false;

    /**
     * 파일 캐시 여부
     */
    private static $isFileCacheuse = false;

    /**
     * JS 태그 출력 순서
     */
    private static $sOrderJs = false;

    /**
     * CSS 태그 출력 순서
     */
    private static $sOrderCss = false;

    /**
     * 멤캐시 호스트 정보
     */
    private static $sMemcacheHost = false;

    /**
     * 멤캐시 포트 정보
     */
    private static $sMemcachePort = false;

    /**
     * 핸들러명(file or memcache)
     */
    private static $sHandler = null;

    /**
     * 인스턴스
     */
    private static $instance = null;

    private function __construct(){}

    /**
     * 인스턴스 반환
     *
     * @return Optimizer
     */
    public static function getInstance()
    {
        if (!self::$instance) self::$instance = new self;

        return self::$instance;
    }

    /**
     * Alias 용
     *
     * @return Optimizer
     */
    public static function instance()
    {
        return self::$instance;
    }

    /**
     * 인스턴스 제거
     */
    public static function instancedel()
    {
        self::$instance = null;
    }

    /**
     * 초기화
     *
     * @param string $sUrl                (웹상에서 호출될 PHP 파일의 URL 경로 (/ 부터 표시 할것))
     * @param string $sCacheroot          (캐시 루트)
     * @param boolean $isFileCacheuse   (파일 캐시 사용여부)
     * @param string $sOrderJs         (JS 출력 순서)
     * @param string $sOrderCss        (CSS 출력 순서)
     * @param string $sHandler         (핸들러명, 기본 file)
     * @param string $sMemcacheHost    (멤캐시 호스트 정보)
     * @param string $sMemcachePort    (멤캐시 포트 정보)
     */
    public function initialize($sUrl=FW_URL_OPTIMIZER, $sCacheroot=FW_DIR_CACHE, $isFileCacheuse=FW_OPTIMIZER_FILE_CACHE_USE, $sOrderJs=FW_OPTIMIZER_ORDER_JS, $sOrderCss=FW_OPTIMIZER_ORDER_CSS, $sHandler=FW_OPTIMIZER_HANDLER, $sMemcacheHost=FW_OPTIMIZER_MEMCACHE_HOST, $sMemcachePort=FW_OPTIMIZER_MEMCACHE_PORT)
    {
        if (!is_dir($sCacheroot)) {
            throw new Exception('Optimizer cacheRoot is not found - ' . $sCacheroot, E_ERROR);
        }

        self::$sHandler = strtolower($sHandler);
        self::$sUrl = $sUrl;
        self::$sCachejsPath = $sCacheroot . '/' . self::TYPE_JS;
        self::$sCachecssPath = $sCacheroot . '/' . self::TYPE_CSS;

        self::_fileCreateCacheDir(self::$sCachejsPath, self::$sCachecssPath);

        self::$aJs = array(self::LOAD_RESERVED_PRE => array(), self::LOAD_BEFORE => array(), self::LOAD_MAIN => array(), self::LOAD_AFTER => array());
        self::$aJsExternal = array(self::LOAD_BEFORE => array(), self::LOAD_MAIN => array(), self::LOAD_AFTER => array());
        self::$aJsSource = array();

        self::$aCss = array(self::LOAD_RESERVED_PRE => array(), self::LOAD_BEFORE => array(), self::LOAD_MAIN => array(), self::LOAD_AFTER => array());
        self::$aCssExternal = array(self::LOAD_BEFORE => array(), self::LOAD_MAIN => array(), self::LOAD_AFTER => array());
        self::$aCssSource = array();

        self::$isFileCacheuse = $isFileCacheuse === true ? true : false;

        self::$sOrderJs = $sOrderJs;
        self::$sOrderCss = $sOrderCss;

        self::$sMemcacheHost = $sMemcacheHost;
        self::$sMemcachePort = $sMemcachePort;

        self::$isProgress = true;

        return true;
    }

    /**
     * 동적 데이터를 받을 수 있는 JS 파일 추가
     *
     * @param string $sRoot    (루트 디렉토리)
     * @param string $sPath    (경로)
     * @param array $aData    (데이터)
     * @return boolean
     */
    public function pJS($sRoot, $sPath, $aData=array(), $isAutoExt=true)
    {
        $sSource = self::_phpFile($sRoot, $sPath, FW_JS_EXT, $aData, $isAutoExt);

        if ($sSource) {
            self::$aJsSource[] = $sSource;
            return true;
        }

        return false;
    }

    /**
     * 동적 데이터를 받을 수 있는 CSS 파일 추가
     *
     * @param string $sRoot    (루트 디렉토리)
     * @param string $sPath    (경로)
     * @param array $aData    (데이터)
     * @return boolean
     */
    public function pCSS($sRoot, $sPath, $aData=array(), $isAutoExt=true)
    {
        $sSource = self::_phpFile($sRoot, $sPath, FW_CSS_EXT, $aData, $isAutoExt);

        if ($sSource) {
            self::$aCssSource[] = $sSource;
            return true;
        }

        return false;
    }

    /**
     * JS 파일 추가
     *
     * @param string $sRoot        (루트 디렉토리)
     * @param string $sPath        (경로)
     * @param string $sAppendType  (추가타입 : before, main, after)
     * @return boolean
     */
    public function addJS($sRoot, $sPath, $sAppendType=null, $isAutoExt=true)
    {
        $sAppendType = self::_getAppendType($sAppendType);

        $sPath = self::_addFile($sRoot, $sPath, FW_JS_EXT, $isAutoExt);

        if ($sPath) {
            self::$aJs[$sAppendType][] = $sPath;
            return true;
        }

        return false;
    }

    /**
     * CSS 파일 추가
     *
     * @param string $sRoot        (루트 디렉토리)
     * @param string $sPath        (경로)
     * @param string $sAppendType  (추가타입 : before, main, after)
     * @return boolean
     */
    public function addCSS($sRoot, $sPath, $sAppendType=null, $isAutoExt=true)
    {
        $sAppendType = self::_getAppendType($sAppendType);

        $sPath = self::_addFile($sRoot, $sPath, FW_CSS_EXT, $isAutoExt);

        if ($sPath) {
            self::$aCss[$sAppendType][] = $sPath;
            return true;
        }

        return false;
    }

    /**
     * JS 소스 추가
     *
     * @param string $sSource
     * @return boolean
     */
    public function writeJS($sSource)
    {
        if ($sSource == '') {
            return false;
        }

        self::$aJsSource[] = $sSource;

        return true;
    }

    /**
     * CSS 소스 추가
     *
     * @param string $sSource
     * @return boolean
     */
    public function writeCSS($sSource)
    {
        if ($sSource == '') {
            return false;
        }

        self::$aCssSource[] = $sSource;

        return true;
    }

    /**
     * 외부 JS URL 주소로 추가
     *
     * @param string $sUrl
     * @param string $sCharset     (기본 : utf-8)
     * @param array $aAttr        (기본 : null) - 태그 속성 배열
     * @param string $sAppendType  (기본 : main) - main, before, after
     * @return boolean
     */
    public function externalJS($sUrl, $sCharset='utf-8', $aAttr=null, $sAppendType=null)
    {
        $sUrlStr = self::_externalFile($sUrl, $sCharset, $aAttr);
        $sAppendType = self::_getAppendType($sAppendType);

        if ($sUrlStr) {
            self::$aJsExternal[$sAppendType][] = $sUrlStr;
            return true;
        }

        return false;
    }

    /**
     * 외부 CSS URL 주소로 추가
     *
     * @param string $sUrl
     * @param string $sCharset     (기본 : utf-8)
     * @param array $aAttr        (기본 : null) - 태그 속성 배열
     * @param string $sAppendType  (기본 : main) - main, before, after
     * @return boolean
     */
    public function externalCSS($sUrl, $sCharset='utf-8', $aAttr=null, $sAppendType=null)
    {
        $sUrlStr = self::_externalFile($sUrl, $sCharset, $aAttr);
        $sAppendType = self::_getAppendType($sAppendType);

        if ($sUrlStr) {
            self::$aCssExternal[$sAppendType][] = $sUrlStr;
            return true;
        }

        return false;
    }

    /**
     * JS 최적화 태그 반환
     *
     * @param string $sQuery (추가 쿼리)
     * @return string
     */
    public function makeOptimizerJsTag($sQuery='')
    {
        $aJs = array_merge(
            array_unique(self::$aJs[self::LOAD_RESERVED_PRE]),
            array_unique(self::$aJs[self::LOAD_BEFORE]),
            array_unique(self::$aJs[self::LOAD_MAIN]),
            array_unique(self::$aJs[self::LOAD_AFTER])
        );

        if (self::$sHandler == 'file' && self::$isFileCacheuse === false) {
            $mCacheTarget = $aJs;
            $sQuery .= '&time=' . self::_getLastMtime($mCacheTarget);
        } else {
            $mCacheTarget = self::_createCache(array(
                'type' => self::$sHandler,
                'packer' => '_packJs',
                'file_root_dir' => self::$sCachejsPath,
                'cache_list' => $aJs,
                'memcache_host' => self::$sMemcacheHost,
                'memcache_port' => self::$sMemcachePort
            ));
        }

        $sO = self::$sOrderJs;
        $sK = array(
            $sO{0} => 0,
            $sO{1} => 1,
            $sO{2} => 2,
        );

        $aTag = array();

        if (isset($sK['O'])) {
            $aTag[$sK['O']] = self::_makeExternalJs();
        }

        if (isset($sK['I'])) {
            $aTag[$sK['I']] = self::_makeTagJs($mCacheTarget, $sQuery);
        }

        if (isset($sK['E'])) {
            $sSource = self::_createSource(self::$aJsSource);
            $aTag[$sK['E']] = self::_makeSourceJs($sSource);
        }

        ksort($aTag);

        $sResult = implode("\n", $aTag);

        return $sResult;
    }

    /**
     * CSS 최적화 태그 반환
     *
     * @param string $sQuery (추가 쿼리)
     * @return string
     */
    public function makeOptimizerCssTag($sQuery='')
    {
        $aCss = array_merge(
            array_unique(self::$aCss[self::LOAD_BEFORE]),
            array_unique(self::$aCss[self::LOAD_MAIN]),
            array_unique(self::$aCss[self::LOAD_AFTER])
        );

        if (self::$sHandler == 'file' && self::$isFileCacheuse === false) {
            $mCacheTarget = $aCss;
            $sQuery .= '&time=' . self::_getLastMtime($mCacheTarget);
        } else {
            $mCacheTarget = self::_createCache(array(
                'type' => self::$sHandler,
                'packer' => '_packCss',
                'file_root_dir' => self::$sCachecssPath,
                'cache_list' => $aCss,
                'memcache_host' => self::$sMemcacheHost,
                'memcache_port' => self::$sMemcachePort
            ));
        }

        $sO = self::$sOrderCss;
        $sK = array(
            $sO{0} => 0,
            $sO{1} => 1,
            $sO{2} => 2,
        );

        $aTag = array();

        if (isset($sK['O'])) {
            $aTag[$sK['O']] = self::_makeExternalCss();
        }

        if (isset($sK['I'])) {
            $aTag[$sK['I']] = self::_makeTagCss($mCacheTarget, $sQuery);
        }

        if (isset($sK['E'])) {
            $sSource = self::_createSource(self::$aCssSource);
            $aTag[$sK['E']] = self::_makeSourceCss($sSource);
        }

        ksort($aTag);

        $sResult = implode("\n", $aTag);

        return $sResult;
    }

    /**
     * 캐시 전체경로에 해당되는 디렉토리를 생성하고 전체 경로를 반환
     * (memcache 타입인 경우 캐시키만을 반환한다.)
     *
     * @param array $aParam array(
     *  'type' => 캐시타입명(file or memcache)
     *  'cache_name' => 캐시명
     *  'file_root_dir' => 파일 캐시 루트 디렉토리
     *  'memcache_host' => 멤캐시 호스트 정보
     *  'memcache_port' => 멤캐시 포트 정보
     * )
     * @return string
     */
    public static function getCacheSavePath($aParam)
    {
        $sMethod = '_' . $aParam['type'] . 'CreateCacheSavePath';

        return call_user_func(array(__CLASS__, $sMethod), $aParam);
    }

    /**
     * 캐시 생성(성공 시 캐시명을 반환)
     *
     * @param array $aParam array(
     *  'type' => 캐시타입명(file or memcache)
     *  'packer' => 캐시명
     *  'file_root_dir' => 파일 캐시 루트 디렉토리
     *  'cache_list' => 캐시 대상 리스트 배열
     * )
     * @return string || false
     */
    private static function _createCache($aParam)
    {
        $sMethod = '_' . $aParam['type'] . 'CreateCache';

        return call_user_func(array(__CLASS__, $sMethod), $aParam);
    }


    /**
     * 태그 속성 배열을 문자열로 변환
     *
     * @param array $aAttr
     * @return string
     */
    private static function _createAttrString($aAttr)
    {
        if (empty($aAttr)) return '';

        $sResult = '';

        foreach ($aAttr as $k => $v) {
            if (preg_match('/["]/', $v) !== 0) {
                $sQuote = "'";
            } else {
                $sQuote = '"';
            }

            $sResult .= sprintf(' %s=%s%s%s', $k, $sQuote, $v, $sQuote);
        }

        return $sResult;
    }

    /**
     * 캐시 처리될 전체 소스 반환
     *
     * @param array $aSource
     * @return string
     */
    private static function _createSource($aSource)
    {
        if (empty($aSource)) return false;

        $sContent = implode("\n", $aSource);

        $sContent = preg_replace('/^[\t\s]*/m', '', $sContent);
        $sContent = "\n".$sContent."\n";

        return $sContent;
    }

    /**
     * 동적 데이터 파일 추가
     *
     * @param string $sRoot
     * @param string $sPath
     * @param string $sDefaultExt
     * @param array $aData
     * @param boolean $isAutoExt
     * @return string | false
     */
    private static function _phpFile($sRoot, $sPath, $sDefaultExt, $aData=array(), $isAutoExt=true)
    {
        $sExt = self::_getExt($sDefaultExt, $isAutoExt);

        if ($sRoot) {
            $sRoot = $sRoot . '/';
        }

        $sFullpath = $sRoot . $sPath . $sExt;

        if (!is_file($sFullpath)) {
            return false;
        }

        $aData = is_array($aData) ? $aData : array();
        $sSource = getFileContentsAutoIconv($sFullpath);

        $aSearch = array();
        $aReplace = array();

        //할당된 데이터와 같은 키의 변수형태가 밸류내부에 존재할 경우 치환되지 않도록 하기 위함
        //일단 {+{$키}+} 이와 같은 형태로 만듬
        $sRegex = '/\{\$([a-z][a-z0-9_]*)\}/i';
        $sSource = preg_replace($sRegex, '{+{\$$1}+}', $sSource);

        $sRegex = '/\{\+\{\$([a-z][a-z0-9_]*)\}\+\}/i';
        if (preg_match_all($sRegex, $sSource, $aMatch) !== 0) {

            foreach ($aMatch[1] as $i => $key) {

                $aSearch[] = $aMatch[0][$i];
                $aReplace[] = $aData[$key];

            }

        }

        $sRegex = '/\{(?:\\\)\$([a-z][a-z0-9_]*)\}/i';
        if (preg_match_all($sRegex, $sSource, $aMatch) !== 0) {

            $aSearch[] = $aMatch[0][0];
            $aReplace[] = '{$'. $aMatch[1][0] . '}';

        }

        $sSource = str_replace($aSearch, $aReplace, $sSource);

        return $sSource;
    }

    /**
     * 외부 파일 추가
     *
     * @param string $sUrl
     * @param string $sCharset
     * @param array $aAddAttr
     * @return string | false
     */
    private static function _externalFile($sUrl, $sCharset='utf-8', $aAddAttr=null)
    {
        if (self::_isExternalUrl($sUrl) === false) {
            return false;
        }

        $sCharset = trim(strtolower($sCharset));
        $aAttrCharset = array('charset' => $sCharset);

        if (empty($aAddAttr)) {
            $aAttr = $aAttrCharset;
        } else {
            $aAttr = array_merge($aAddAttr, $aAttrCharset);
        }

        $sAttrs = self::_createAttrString($aAttr);

        return $sUrl . self::SP . $sAttrs;
    }

    /**
     * 일반 파일 추가
     *
     * @param string $sRoot
     * @param string $sPath
     * @param string $sDefaultExt
     * @param boolean $isAutoExt
     * @param string $sAppendType
     * @return string | false
     */
    private static function _addFile($sRoot, $sPath, $sDefaultExt, $isAutoExt=true)
    {
        $sExt = self::_getExt($sDefaultExt, $isAutoExt);

        if ($sRoot) {
            $sRoot = $sRoot . '/';
        }

        $sFullpath = $sRoot . $sPath . $sExt;

        if (!is_file($sFullpath)) {
            return false;
        }

        return preg_replace('@[/\\\]+@', '/', $sFullpath);
    }

    /**
     * 확장자 반환
     *
     * @param string $sDefault
     * @param boolean $isAutoExt
     * @return string
     */
    private static function _getExt($sDefault, $isAutoExt=true)
    {
        $sExt = $isAutoExt === true ? '.' . $sDefault : '';

        return $sExt;
    }

    /**
     * 추가 타입 반환
     *
     * @param string $sAppendType
     * @return string
     */
    private static function _getAppendType($sAppendType=null)
    {
        $sAppendType = ($sAppendType === null) ? self::LOAD_MAIN : strtolower($sAppendType);

        return $sAppendType;
    }

    /**
     * 가장 최근에 수정된 파일의 mtime 값 반환
     *
     * @param array $aFile
     * @return int
     */
    private static function _getLastMtime($aFile)
    {
        $iLast = 0;
        foreach ($aFile as $f) {
            $mtime = filemtime($f);
            if($iLast < $mtime) $iLast = $mtime;
        }

        return $iLast;
    }

    /**
     * 전체 캐시 파일 네임 반환
     *
     * @param array $aFiles
     * @return string
     */
    private static function _cachedName($aFile)
    {
        $iLast = self::_getLastMtime($aFile);

        $sFilename = implode(',', $aFile);

        return sha1($sFilename.self::$sVersion) . '_' . $iLast;
    }

    /**
     * 캐시 생성 경로 반환
     * (반환되는 값은 파일경로에 대한 전체경로를 뜻합니다.)
     *
     * @param array(
     *  'cache_name' => 캐시명,
     *  'file_root_dir' => 캐시 생성 루트 디렉토리
     * )
     * @return string
     */
    private static function _fileCreateCacheSavePath($aParam)
    {
        $iDepth = (int)FW_OPTIMIZER_CACHE_DIR_LEVEL;
        $sDir = $aParam['file_root_dir'];

        if ($iDepth <= 0) {
            return $sDir . '/' . $aParam['cache_name'];
        }

        $iDepth = $iDepth > 3 ? 3 : $iDepth;

        for ($i=0; $i < $iDepth; ++$i) {
            $sDir .= '/' . $aParam['cache_name'][$i];
        }

        if (!is_dir($sDir)) {
            mkdir($sDir, 0777, true);
        }

        return $sDir . '/' . $aParam['cache_name'];
    }

    /**
     * 캐시 생성(성공 시 캐시명을 반환)
     *
     * @param array(
     *  'packer' => 패킹명,
     *  'file_root_dir' => 캐시 생성 루트 디렉토리
     *  'cache_list' => 캐시대상 파일 리스트 배열
     * )
     * @return string
     */
    private static function _fileCreateCache($aParam)
    {
        if (empty($aParam['cache_list'])) return false;

        $sCachename = self::_cachedName($aParam['cache_list']);
        $sFullpath = self::_fileCreateCacheSavePath(array(
            'file_root_dir' => $aParam['file_root_dir'],
            'cache_name' => $sCachename
        ));

        if (is_file($sFullpath)) {
            return $sCachename;
        }

        $sContent = '';

        foreach ($aParam['cache_list'] as $f) {
            $sContent .= getFileContentsAutoIconv($f) . "\n";
        }

        $sContent = call_user_func(array(__CLASS__, $aParam['packer']), $sContent);

        $result = file_put_contents($sFullpath, $sContent);

        return $result !== false ? $sCachename : false;
    }

    /**
     * 캐시 디렉토리 생성
     *
     * @param string $sDir1
     * @param string $sDir2
     * @return boolean
     */
    private static function _fileCreateCacheDir($sDir1, $sDir2)
    {
        if (!is_dir($sDir1)) {
            mkdir($sDir1);
        }

        if (!is_dir($sDir2)) {
            mkdir($sDir2);
        }

        return true;
    }

    /**
     * 캐시 생성 경로 반환
     * (memcache 타입은 캐시키만을 반환)
     *
     * @param array(
     *     'cache_name' => 캐시명
     * )
     * @return string
     */
    private static function _memcacheCreateCacheSavePath($aParam)
    {
        return $aParam['cache_name'];
    }

    /**
     * 캐시 생성(성공 시 캐시명을 반환)
     *
     * @param array(
     *     'packer' => 패킹명,
     *  'file_root_dir' => 캐시 생성 루트 디렉토리
     *  'cache_list' => 캐시대상 파일 리스트 배열
     * )
     * @return string
     */
    private static function _memcacheCreateCache($aParam)
    {
        if (empty($aParam['cache_list'])) return false;

        $Memcache = self::_memcacheCreateConn($aParam['memcache_host'], $aParam['memcache_port']);

        $sCachename = self::_cachedName($aParam['cache_list']);
        $sFullpath = self::_memcacheCreateCacheSavePath(array(
            'cache_name' => $sCachename
        ));

        if ($Memcache->get($sFullpath) !== false) {
            return $sCachename;
        }

        $sContent = '';

        foreach ($aParam['cache_list'] as $f) {
            $sContent .= getFileContentsAutoIconv($f) . "\n";
        }

        $sContent = call_user_func(array(__CLASS__, $aParam['packer']), $sContent);

        $result = $Memcache->set($sFullpath, $sContent, false);

        return $result !== false ? $sCachename : false;
    }

    /**
     * 멤캐시 커넥션 객체 반환
     *
     * @param string $sHost
     * @param string $sPort
     * @return Memcache
     */
    private static function _memcacheCreateConn($sHost, $sPort)
    {
        $Memcache = new Memcache();

        $isConnect = $Memcache->connect($sHost, $sPort);

        if ($isConnect === false) {
            throw new Exception('[optimizer] can not connect to memcache host - ' . $sHost . ':' . $sPort);
        }

        return $Memcache;
    }

    /**
     * JS 압축
     *
     * @param string $sContent
     * @return string
     */
    private static function _packJs($sContent)
    {
        return $sContent;
    }

    /**
     * CSS 압축
     *
     * @param string $sContent
     * @return string
     */
    private static function _packCss($sContent)
    {
        $sContent = CSSMin::minify($sContent);

        return $sContent;
    }

    /**
     * 외부 스크립트로 첨부할 수 있는 URL 주소형식인지 파악
     *
     * @param string $sUrl
     * @return boolean
     */
    private static function _isExternalUrl($sUrl)
    {
        $result = preg_match('@^(http://|https://|//)@i', $sUrl);

        return ($result !== 0) ? true : false;
    }

    /**
     * JS 태그
     *
     * @param string $sCachename
     * @param string $sQuery
     * @return string
     */
    private static function _makeTagJs($sCachename, $sQuery)
    {
        if (!$sCachename) return '';

        $sResult = '';

        if (is_array($sCachename)) {

            foreach ($sCachename as $name) {
                $sResult .= self::_makeTagJs(utilDes::encrypt($name), $sQuery) . "\n";
            }

        } else {

            $sResult = sprintf('<script type="text/javascript" src="%s?filename=%s&type=%s&%s"></script>', self::$sUrl, $sCachename, self::TYPE_JS, $sQuery);

        }

        return $sResult;
    }

    /**
     * JS 태그(인라인버젼)
     *
     * @param string $sSource
     * @return string
     */
    private static function _makeSourceJs($sSource)
    {
        if (!$sSource) return '';

        return sprintf('<script type="text/javascript">%s</script>', $sSource);
    }

    /**
     * 외부 JS URL 생성 태그
     */
    private static function _makeExternalJs()
    {
        if (empty(self::$aJsExternal)) return '';

        $aExternal = array_merge(
            array_unique(self::$aJsExternal[self::LOAD_BEFORE]),
            array_unique(self::$aJsExternal[self::LOAD_MAIN]),
            array_unique(self::$aJsExternal[self::LOAD_AFTER])
        );

        $sResult = '';
        foreach ($aExternal as $ext) {
            $aUrl = explode(self::SP, $ext);

            $sResult .= sprintf("<script type=\"text/javascript\" src=\"%s\"%s></script>\n", $aUrl[0], $aUrl[1]);
        }

        return $sResult;
    }

    /**
     * 외부CSS URL 생성 태그
     */
    private static function _makeExternalCss()
    {
        if (empty(self::$aCssExternal)) return '';

        $aExternal = array_merge(
            array_unique(self::$aCssExternal[self::LOAD_BEFORE]),
            array_unique(self::$aCssExternal[self::LOAD_MAIN]),
            array_unique(self::$aCssExternal[self::LOAD_AFTER])
        );

        $sResult = '';
        foreach ($aExternal as $ext) {
            $aUrl = explode(self::SP, $ext);

            $sResult .= sprintf("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\"%s/>\n", $aUrl[0], $aUrl[1]);
        }

        return $sResult;
    }

    /**
     * CSS 태그
     *
     * @param string $sCachename
     * @param string $sQuery
     * @return string
     */
    private static function _makeTagCss($sCachename, $sQuery)
    {
        if (!$sCachename) return '';

        $sResult = '';

        if (is_array($sCachename)) {

            foreach ($sCachename as $name) {
                $sResult .= self::_makeTagCss(utilDes::encrypt($name), $sQuery) . "\n";
            }

        } else {

            $sResult = sprintf('<link rel="stylesheet" type="text/css" href="%s?filename=%s&type=%s&%s" />', self::$sUrl, $sCachename, self::TYPE_CSS, $sQuery);

        }

        return $sResult;
    }

    /**
     * CSS 태그(인라인버젼)
     *
     * @param string $sSource
     * @return string
     */
    private static function _makeSourceCss($sSource)
    {
        if (!$sSource) return '';

        return sprintf('<style type="text/css">%s</style>', $sSource);
    }
}
